package com.superdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.superdp.dto.Result;
import com.superdp.entity.Shop;
import com.superdp.mapper.ShopMapper;
import com.superdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.superdp.utils.CacheClient;
import com.superdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.superdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;
    /**
     * 查询店铺，主动更新为主，逻辑过期兜底解决双写一致
     * 使用缓存空对象解决缓存穿透
     * 使用不同key的TTL添加随机值解决缓存雪崩
     * 使用逻辑过期和互斥锁解决缓存击穿
     * 封装缓存工具栏CacheClient，节省后续开发成本
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
//        Shop shop =cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,
//                CACHE_SHOP_TTL,TimeUnit.MINUTES);

       Shop shop = cacheClient.queryWithLogicExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        if(shop==null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }


    /**
     * 缓存重建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR=Executors.newFixedThreadPool(10);
    /**
     * 逻辑过期时间解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithLogicExpire(Long id){
        //获取key
        String key=CACHE_SHOP_KEY+id;
        //根据key查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //命中缓存
        if(shopJson!=null){
            //查看是否为空字符串
            if(StrUtil.isBlank(shopJson)){
                //是的话直接返回null
                return null;
            }
            //不为空字符串判断逻辑过期时间是否过期了
            RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
            //Data转成Shop
            Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            //逻辑过期时间在当前时间后边，未过期，直接返回
            if(expireTime.isAfter(LocalDateTime.now())){
                return shop;
            }
            //过期了，尝试获取锁
            String lockKey=LOCK_SHOP_KEY+id;
            try {
                boolean b = tryLock(lockKey);
                if(!b){
                    //获取锁失败，返回过期的数据
                    return shop;
                }
                //获取锁成功，重新开启一个线程去重建缓存
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    //查询数据库最新的信息
                    try {
                        //模拟缓存重建的时间
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    saveShop2Redis(id,20L);
                });
                return shop;
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unLock();
            }
        }
        //未命中缓存直接返回null
        return null;
    }
    /**
     * 互斥锁解决缓存击穿问题
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        //获取key
        String key=CACHE_SHOP_KEY+id;
        //根据key查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //命中缓存
        if(shopJson!=null){
            //查看是否为空字符串
            if(StrUtil.isBlank(shopJson)){
                //是的话直接返回null
                return null;
            }
            //不为空字符串则正常返回shop
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //未命中缓存先获取锁
        String lockKey=LOCK_SHOP_KEY+id;
        try{
            boolean b = tryLock(lockKey);
            if(!b){
                //获取锁失败先暂停一会儿，之后重试获取锁
                Thread.sleep(50);
                return queryWithPassThrough(id);
            }
            Shop shop = query().eq("id", id).one();
            //数据库也没有就返回null
            if(shop==null){
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //缓存重建
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.SECONDS);
            return shop;
        }catch (Exception e){
            throw new RuntimeException(e);
        }finally {
            unLock();
        }
    }

    /*
    获取锁，使用redis中String数据结构的setnx命令
     */
    private boolean tryLock(String lockKey) {
        Boolean b = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(b);
    }

    /**
     * 释放锁
     */
    private void unLock(){
        stringRedisTemplate.delete(LOCK_SHOP_KEY);
    }

    /**
     * 解决缓存穿透问题
     * @param id
     * @return
     */
    public Shop queryWithPassThrough(Long id){
        //获取key
        String key=CACHE_SHOP_KEY+id;
        //根据key查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //命中缓存
        if(shopJson!=null){
            //查看是否为空字符串
            if(StrUtil.isBlank(shopJson)){
                //是的话直接返回null
                return null;
            }
            //不为空字符串则正常返回shop
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //未命中缓存查询数据库
        Shop shop = query().eq("id", id).one();
        //数据也没有直接返回null
        if(shop==null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //缓存重建
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.SECONDS);

        return shop;
    }


    /**
     * 主动更新
     * 先操作数据库再删除缓存
     * @param shop
     * @return
     */
    @Transactional
    @Override
    public Result update(Shop shop) {
        //店铺id
        Long id = shop.getId();
        //先更新数据库
        updateById(shop);

        if(id==null){
            return Result.fail("店铺id不能为空");
        }
        //再删除缓存
        String key=CACHE_SHOP_KEY+id;
        stringRedisTemplate.delete(key);
        return null;
    }

    /**
     * 缓存预热，将数据存入redis，不过存入的时逻辑过期时间
     * @param id
     * @param expireTime
     */
    @Override
    public void saveShop2Redis(Long id, Long expireTime) {
        Shop shop = query().eq("id", id).one();
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));

        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }


}
